package com.csthink.web.infrastructure.security.config;

import com.csthink.common.application.ResponseWrapper;
import com.csthink.common.infrastructure.enums.ErrorEnum;
import com.csthink.web.infrastructure.annotations.IgnoreWebSecurity;
import com.csthink.web.infrastructure.security.AuthenticationHelper;
import com.csthink.web.infrastructure.security.authentication.filter.JwtAuthenticationFilter;
import com.csthink.web.infrastructure.security.authentication.firewall.SuppressStackTraceHttpFirewall;
import com.csthink.web.infrastructure.security.authentication.provider.JwtAuthenticationProvider;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.Resource;
import java.util.Map;
import java.util.Set;

import static com.csthink.web.infrastructure.security.AuthenticationHelper.FULLY_IGNORE_URL_LIST;

/**
 * 注解 @EnableWebSecurity 用于开启 Web 安全性支持, Spring Security 的默认配置将会被应用
 * 注解 @EnableGlobalMethodSecurity 后可以启用方法级权限控制，它提供了prePostEnabled 、securedEnabled 和 jsr250Enabled 三种不同的机制
 * prePostEnabled = true 会解锁 @PreAuthorize 和 @PostAuthorize 两个注解。从名字就可以看出@PreAuthorize 注解会在方法执行前进行验证，而 @PostAuthorize 注解会在方法执行后进行验证。
 *
 * @author <a href="mailto:security.2009@live.cn">Mars</a>
 * @since 2023-09-12
 */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Value("${jwt.filter.apis}")
    private String excludeApiPath;

    //@Autowired
    //private LoginFilter loginFilter;

    @Resource
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Bean
    public AuthenticationHelper authenticationHelper() {
        return new AuthenticationHelper();
    }

    /**
     * In this provider, we just return the Authentication object.
     *
     * @return
     */
    @Bean
    public JwtAuthenticationProvider jwtAuthenticationProvider() {
        return new JwtAuthenticationProvider();
    }

    private JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        return new JwtAuthenticationFilter(authenticationManager(), authenticationHelper());
    }

    @Bean
    public FilterRegistrationBean<JwtAuthenticationFilter> jwtFilter() throws Exception {
        final FilterRegistrationBean<JwtAuthenticationFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(jwtAuthenticationFilter());
        registrationBean.setEnabled(false);
        registrationBean.setOrder(Ordered.LOWEST_PRECEDENCE - 2); // see this method's javadoc Order
        return registrationBean;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * JwtAuthenticationToken put role permission to credentials which will erased by ProviderManager.
     * TODO: we may change this behavior after the new authorization flow finished.
     *
     * @throws Exception
     */
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        auth.eraseCredentials(false);
    }

    /**
     * 启动过滤器链是很昂贵的，占用了系统很多资源，有时候我们经过一个路径（比如访问静态资源：图片，视频等），不需要进行认证和授权，
     * 也就不需要启动过滤器链，为了节约系统资源，可以通过重写configure(WebSecurity web)方法来禁用过滤器链
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        WebSecurity.IgnoredRequestConfigurer ignoring = web.ignoring();
        // Hard code 方式1：
        ignoring.antMatchers(FULLY_IGNORE_URL_LIST);

        // 基于配置方式: (适合静态资源文件)
        //String[] pathArray = excludeApiPath.trim().split(",");
        //ignoring.antMatchers(pathArray);

        // Annotation 方式:  @IgnoreWebSecurity
        ignoring.antMatchers(HttpMethod.GET, "/hello/hardcode");
        this.ignoreAnnotation(ignoring, this.requestMappingHandlerMapping);

        web.httpFirewall(new SuppressStackTraceHttpFirewall());
    }

    /**
     * 基于注解的忽略拦截
     *
     * @param ignoring
     * @param requestMappingHandlerMapping
     */
    private void ignoreAnnotation(WebSecurity.IgnoredRequestConfigurer ignoring, RequestMappingHandlerMapping requestMappingHandlerMapping) {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
            HandlerMethod handlerMethod = entry.getValue();
            if (handlerMethod.hasMethodAnnotation(IgnoreWebSecurity.class)) {
                Set<String> patternValues = entry.getKey().getPatternValues();
                Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods();
                if (CollectionUtils.isEmpty(methods)) {
                    // RequestMapping没有指定method
                    ignoring.antMatchers(patternValues.toArray(new String[0]));
                } else {
                    for (RequestMethod method : methods) {
                        // RequestMapping指定了method
                        ignoring.antMatchers(HttpMethod.resolve(method.name()), patternValues.toArray(new String[0]));
                    }
                }
            }
        }
    }


    /**
     * 重写 configure(HttpSecurity http)让我们可以配置认证和授权，也就是说走到这个方法的时候，是经过了过滤器链的。
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * CSRF攻击对于无状态应用（前后端分离，使用token，天然免疫）来说是无效的，只有Session类应用需要去预防
         * 当进行登录的时候，如果没有禁用CSRF配置，那么每个POST请求必须携带一个CSRF Token，否则不予授权
         *
         * 不设置X-Frame-Options header， 禁用 X-Frame-Options 的保护措施(disabled: 允许页面被嵌入到 iframe 中)
         * .headers().frameOptions().disable()
         *
         * 启用 xss保护
         * .headers().xssProtection().xssProtectionEnabled(true).block(true)
         *
         * 关闭表单登录，禁用生成默认的登录页面
         * .formLogin(AbstractHttpConfigurer::disable)
         *
         * 关闭 httpBasic 采用自定义过滤器
         * .httpBasic(AbstractHttpConfigurer::disable)
         *
         * 禁用生成默认的logout注销页面
         * .logout(AbstractHttpConfigurer::disable)
         * .anonymous(AbstractHttpConfigurer::disable)
         *
         * 在 JwtAuthenticationFilter 实现
         * .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
         */

        http.csrf(AbstractHttpConfigurer::disable)
                .headers(AbstractHttpConfigurer::disable)
                // 使用jwt 的前后端分离项目是无状态的，Spring Security 不需要使用 session 机制
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 关闭跨域
                .cors(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .anonymous(AbstractHttpConfigurer::disable)
                .authorizeRequests(req -> req
                        // 登陆页和注册页允许公开访问
                        .antMatchers("/v1/passport/login", "/v1/passport/register").permitAll()
                        // 其它所有接口需要认证
                        .anyRequest().authenticated()
                )
                // 使用自定义的 JWT 认证过滤器替换默认的认证过滤器
                .addFilterAfter(jwtFilter().getFilter(), UsernamePasswordAuthenticationFilter.class)
                //.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
                // SpringSecurity 授权错误处理器,用户无权限访问接口时触发
                .exceptionHandling()
                .accessDeniedHandler(((request, response, accessDeniedException) -> {
                    String errorMessage = "Forbidden: No permission for: " + request.getMethod() + " " + request.getRequestURI();
                    log.warn(errorMessage);
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    ResponseWrapper.responseError(ErrorEnum.FORBIDDEN, errorMessage, response);
                }));
    }

}
